<template>
  <div class="codePanel">
    <div class="yaml">
      <textarea ref="codemirror"></textarea>
    </div>
    <div class="result hide">
      <textarea ref="result"></textarea>
    </div>
    <div class="btn" @click="toggleResultShow()">
      <img
        width="20"
        src=""
      />
    </div>
  </div>
</template>

<script>
// 引入CodeMirror和基础样式
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
// JSON代码高亮需要由JavaScript插件支持
import 'codemirror/mode/yaml/yaml.js'
// JSON代码高亮需要由JavaScript插件支持
import 'codemirror/mode/javascript/javascript.js'
// 选择IDEA主题样式，还有其他很多主题可选
import 'codemirror/theme/idea.css'
// 自动刷新依赖
import 'codemirror/addon/display/autorefresh'
// 行高亮依赖
import 'codemirror/addon/selection/active-line'
// 支持使用Sublime快捷键
import 'codemirror/keymap/sublime.js'
// 搜索功能的依赖
import 'codemirror/addon/dialog/dialog.js'
import 'codemirror/addon/dialog/dialog.css'
// 支持搜索功能
import 'codemirror/addon/search/search'
import 'codemirror/addon/search/searchcursor.js'
// 支持各种代码折叠
import 'codemirror/addon/fold/foldgutter.css'
import 'codemirror/addon/fold/foldcode.js'
import 'codemirror/addon/fold/foldgutter.js'
import 'codemirror/addon/fold/brace-fold.js'
import 'codemirror/addon/fold/comment-fold.js'
// 支持代码区域全屏功能
import 'codemirror/addon/display/fullscreen.css'
import 'codemirror/addon/display/fullscreen.js'
// 支持括号自动匹配
import 'codemirror/addon/edit/matchbrackets.js'
import 'codemirror/addon/edit/closebrackets.js'
// 支持代码自动补全
import 'codemirror/addon/hint/show-hint.css'
import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/anyword-hint.js'
// 行注释
import 'codemirror/addon/comment/comment.js'
// 语法校验基础依赖
import 'codemirror/addon/lint/lint.css'
import 'codemirror/addon/lint/lint.js'
// 需要依赖全局的jsonlint，不是很优雅
import 'codemirror/addon/lint/yaml-lint.js'
// 需要依赖全局的jsonlint，不是很优雅
import 'codemirror/addon/lint/json-lint.js'
// 引入jsonlint
import jsonlint from 'jsonlint-mod'
import jsyaml from 'js-yaml'
import { markRaw, nextTick } from 'vue'
import { debounce } from 'lodash-es'

window.jsyaml = jsyaml
window.jsonlint = jsonlint

const SexyYamlType = new jsyaml.Type('!sexy', {
  kind: 'sequence', // See node kinds in YAML spec: http://www.yaml.org/spec/1.2/spec.html#kind//
  construct: function (data) {
    return data.map(function (string) {
      return 'sexy ' + string
    })
  }
})
const SEXY_SCHEMA = jsyaml.DEFAULT_SCHEMA.extend([SexyYamlType])
let codemirror, result

export default {
  name: 'YamlEditor',
  props: {
    modelValue: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      isInit: false
    }
  },
  watch: {
    modelValue: {
      handler() {
        if (this.modelValue && codemirror && !this.isInit) {
          nextTick(() => {
            codemirror.setValue(this.modelValue)
          })
          this.isInit = true
        }
      },
      immediate: true
    }
  },
  mounted() {
    // 防止转为监听对象，vue3中如果CodeMirror对象被转为监听对象，会无法正常使用
    codemirror = markRaw(
      // 用ref获取需要渲染的textarea的DOM
      CodeMirror.fromTextArea(this.$refs.codemirror, {
        // JS高亮显示
        mode: 'yaml',
        indentUnit: 2, // 缩进单位，默认2
        smartIndent: true, // 是否智能缩进
        // 显示行号
        styleActiveLine: true,
        lineNumbers: true,
        // 设置主题
        theme: 'idea',
        // 绑定sublime快捷键
        keyMap: 'sublime',
        // 开启代码折叠
        lineWrapping: true,
        foldGutter: true,
        gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
        // CodeMirror-lint-markers是实现语法报错功能
        lint: true,

        // 全屏模式
        fullScreen: false,

        // 括号匹配
        matchBrackets: true,
        autoCloseBrackets: true,

        // 额外快捷键
        extraKeys: {
          F11: (cm) => {
            cm.setOption('fullScreen', !cm.getOption('fullScreen'))
          },
          Esc: (cm) => {
            if (cm.getOption('fullScreen')) {
              cm.setOption('fullScreen', false)
            }
          },
          Tab: function (cm) {
            if (cm.somethingSelected()) {
              // 存在文本选择
              cm.indentSelection('add') // 正向缩进文本
            } else {
              // 无文本选择
              //cm.indentLine(cm.getCursor().line, "add");  // 整行缩进 不符合预期
              // 光标处插入 indentUnit 个空格
              cm.replaceSelection(Array(cm.getOption('indentUnit') + 1).join(' '), 'end', '+input')
            }
          },
          'Shift-Tab': function (cm) {
            // 反向缩进
            if (cm.somethingSelected()) {
              cm.indentSelection('subtract') // 反向缩进
            } else {
              // cm.indentLine(cm.getCursor().line, "subtract");  // 直接缩进整行
              var cursor = cm.getCursor()
              cm.setCursor({ line: cursor.line, ch: cursor.ch - 4 }) // 光标回退 indexUnit 字符
            }
            return
          }
        }
      })
    )
    result = markRaw(
      CodeMirror.fromTextArea(this.$refs.result, {
        readOnly: true,
        // JS高亮显示
        mode: 'application/json'
      })
    )

    // 将编辑器中的值存储下来
    codemirror.on('change', (cm) => {
      this.debounceParse(cm)
    })
    if (!this.isInit && this.modelValue) {
      nextTick(() => {
        codemirror.setValue(this.modelValue)
      })
      this.isInit = true
    }
  },
  beforeUnmount() {
    this.destroy()
  },
  methods: {
    destroy() {
      // 获取代表编辑器的DOM
      const element = codemirror.doc.cm.getWrapperElement()
      // 删除编辑器实例
      element && element.remove && element.remove()
      // 获取代表编辑器的DOM
      const element2 = result.doc.cm.getWrapperElement()
      // 删除编辑器实例
      element2 && element2.remove && element2.remove()
    },
    debounceParse: debounce(function (cm) {
      this.$emit('update:modelValue', cm.getValue())
      this.parse()
    }, 500),
    parse() {
      try {
        const obj = jsyaml.load(codemirror.getValue(), { schema: SEXY_SCHEMA })
        result.setOption('mode', 'javascript')
        result.setValue(JSON.stringify(obj, '\n', 2))
      } catch (err) {
        var dst = this.$refs.result.parentNode
        dst.classList.remove('hide')
        result.setOption('mode', 'text/plain')
        result.setValue(err.message || String(err))
      }
    },
    toggleResultShow() {
      var dst = this.$refs.result.parentNode
      if (dst.classList.contains('hide')) {
        dst.classList.remove('hide')
        this.parse()
      } else {
        dst.classList.add('hide')
      }
    }
  }
}
</script>
<style lang="less" scoped>
.codePanel {
  height: 100%;
  position: relative;
  .yaml {
    width: 100%;
    height: 100%;
    position: relative;
  }
  .result {
    position: absolute;
    right: 0;
    bottom: 0;
    width: 45%;
    height: 100%;
    z-index: 2001;

    &.hide {
      display: none;
    }

    :deep(.CodeMirror) {
      background-color: #f8f8f8;
      border: 1px solid #eee;
      border-radius: 3px;
    }
  }
  .btn {
    position: absolute;
    right: 20px;
    bottom: 20px;
    padding: 5px;
    background: rgba(0, 0, 0, 0.1);
    border-radius: 5px;
    line-height: 0;
    cursor: pointer;
    z-index: 2002;
  }
  textarea {
    width: 100%;
    height: 100%;
  }
  :deep(.CodeMirror) {
    height: 100%;
  }
}
</style>
